/*
   Copyright 2009 Abdulla Abdurakhmanov (abdulla@latestbit.com)
   Original sources are available at www.latestbit.com

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
package org.lineargs;

import java.io.OutputStream;

import java.io.PrintStream;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import java.lang.reflect.ParameterizedType;

import java.util.Collection;
import java.util.LinkedList;
import java.util.StringTokenizer;

import org.lineargs.constraints.RangeConstraint;
import org.lineargs.constraints.RegexConstraint;

/**
* @mainpage LineArgs
* @version 1.0.0b
* @author Abdulla G. Abdurakhmanov (abdulla.abdurakhmanov\@gmail.com).
* @date (c) 2006
*
* @section Introduction Intro
* The library contain a simple implementation of command line arguments parser based on annotations/metadata.
* @section Features
*  - Simple and ease-to-use
*  - Flexible and based on annotation 
*  @section GlobalNotes Notes
*   <i>This documentation describe only API of library, without any
*   internal implementation descriptions.</i>
*/


/**
 * Command line arguments parser implementation
 */
public class LineArgsParser {

    /**
     * Constructor
     */
    public LineArgsParser() {
    }

    /** Do parse arguments
     * <p>Example:</p>
     * @code
     *   public class MyClass {
     *      @StringOption (name = "--optionName", description = "option description")
     *      String myOption = null;
     *      
     *      void setMyOption(String option) {...};
     *      String getMyOption(void) {...};
     *      
     *   }
     *   ...
     *   // Creating parser
     *   LineArgsParser parser = new LineArgsParser();
     *   
     *   // do parse
     *   MyClass object = parser.parse(MyClass.class,args);
     * @endcode
     * @param objClass An object class which contains of annotated properties. 
     * @param args Command line arguments
     * @return Object which contains a filled properties
     */
    public <T> T parse(Class<T> objClass, String args[]) throws Exception {        
        T object = objClass.newInstance();        
        return parse(object, args);
    }


    /**
     * Do parse arguments
     * <p>Example:</p>
     * @code
     *   public class MyClass {
     *      @StringOption (name = "--optionName", description = "option description")
     *      String myOption = null;
     *      
     *      void setMyOption(String option) {...};
     *      String getMyOption(void) {...};
     *      
     *   }
     *   ...
     *   // Creating parser
     *   LineArgsParser parser = new LineArgsParser();
     *   // Manual created object
     *   MyClass object = new MyClass();
     *   // do parse
     *   parser.parse(MyClass,args);
     * @endcode
     * @param object An object which contains of annotated properties. 
     * @param args Command line arguments
     * @return Object which contains a filled properties
     */
    public <T> T parse(T object, String args[]) throws Exception {
        parseArguments(object, args);        
        return object;
    }
        
    protected void parseArguments(Object object, String[] args) throws Exception {
        Collection<Field> installedFields = new LinkedList<Field>();
        int currentArg = 0;        
        while(currentArg<args.length) {
            //for(Field field  : object.getClass().getDeclaredFields()) {
            Field[] fields = object.getClass().getDeclaredFields();
            int idx=0; 
             for(idx=0; idx<fields.length;idx++) {
                Field field = fields[idx];
                String arg = args[currentArg];
                String name = getFieldName(field);
                String shortName = getFieldShortName(field);                
                if( !installedFields.contains(field) &&
                    ((name!=null && arg.equals(name)) || ( shortName!=null && shortName.length()>0 && arg.equals(shortName)))) {
                    currentArg = parseArgument(object,field,args,currentArg);
                    //currentArg++;
                    installedFields.add(field);
                    break;
                }
                else
                if(field.isAnnotationPresent(NonameOption.class) && !installedFields.contains(field) ) {
                        currentArg = parseNoname(object,field,args,currentArg);
                        //currentArg++;
                        installedFields.add(field);
                        break;
                }
            }
            if(idx == fields.length) 
                throw new Exception ("Unknown option: " + args[currentArg]);
            
            currentArg++;
        }
        checkMandatoryFields(object,installedFields);
    }

    protected int parseArgument(Object object, Field field, String[] args,  int currentArg) throws Exception {
        if(field.isAnnotationPresent(ListOption.class)) {
            currentArg = parseSeparated(object,field,args,currentArg);
        }
        else
        if(field.isAnnotationPresent(Option.class)) {
            currentArg = parseOption(object,field,args,currentArg);
        }        
        return currentArg;
    }

    protected int parseOption(Object object, Field field, String[] args,  int currentArg) throws Exception{
        Option meta = field.getAnnotation(Option.class);
        Class<?> objClass = field.getType();
        Object value = null;
        if(!objClass.equals(Boolean.class)) {
            if(++currentArg >=args.length) {
                throw new Exception("Value for option " + meta.name() +" is unspecified!");
            }        
            Constructor<?> constructor = objClass.getConstructor( String.class );
            value = constructor.newInstance(args[currentArg]);
        }
        else {
            value = true;
        }
        checkConstraints(meta.name(),value,field);
        setFieldValue(object,field,value);
        return currentArg;    
    }
    
    protected void checkConstraints(String optionName,Object obj, Field field) throws Exception {
        if(obj instanceof Integer) {
            Integer value = (Integer)obj;
            if(field.isAnnotationPresent(RangeConstraint.class)) {
                RangeConstraint constraint = field.getAnnotation(RangeConstraint.class);
                if(value > constraint.maxValue() || value < constraint.minValue() )
                    throw new Exception("Specified value for option " +optionName +" is incorrect!");
            }
        }
        else {
            if(field.isAnnotationPresent(RangeConstraint.class)) {
                String strValue = obj.toString();
                RangeConstraint constraint = field.getAnnotation(RangeConstraint.class);
                if(strValue.length() > constraint.maxValue() || strValue.length() < constraint.minValue() )
                    throw new Exception("Specified value for option " +optionName +" has incorrect length!");
            }            
        }
        if(field.isAnnotationPresent(RegexConstraint.class)) {
            String strValue = obj.toString();
            RegexConstraint constraint = field.getAnnotation(RegexConstraint.class);
            if( !strValue.matches(constraint.mask()) )
                throw new Exception("Specified value for option " +optionName +" is incorrect!");
        }        
    }

    protected int parseSeparated(Object object, Field field, String[] args,  int currentArg) throws Exception {
        ListOption meta = field.getAnnotation(ListOption.class);
        if(++currentArg >=args.length) {
            throw new Exception("Value for option " + meta.name() +" is unspecified!");
        }
        String value = args[currentArg];
        StringTokenizer strTokenizer = new StringTokenizer(value,meta.delimeter(),false);            
        ParameterizedType tp = (ParameterizedType)field.getGenericType();
        Class<?> paramType = (Class)tp.getActualTypeArguments()[0];
        Collection<Object> param = new LinkedList<Object>();
        while(strTokenizer.hasMoreTokens()) {
            String token = strTokenizer.nextToken();
            Constructor<?> constructor = paramType.getConstructor( String.class );
            Object tokenValue = constructor.newInstance(token);
            checkConstraints(meta.name(),tokenValue,field);                
            param.add(tokenValue);
        }
        setFieldValue(object,field,param);
        return currentArg;
    }

    protected int parseNoname(Object object, Field field, String[] args,  int currentArg) throws Exception {
        NonameOption meta = field.getAnnotation(NonameOption.class);
        Class<?> objClass = field.getType();
        Constructor<?> constructor = objClass.getConstructor( String.class );
        Object value = constructor.newInstance(args[currentArg]);
        checkConstraints("<value>",value,field);
        setFieldValue(object,field,value);
        return currentArg;    
    }

    protected void checkMandatoryFields(Object object, Collection<Field> installedFields) throws Exception {
        for(Field field  : object.getClass().getDeclaredFields()) {
            if(!isFieldOptional(field) && !installedFields.contains(field) ) {
                if(field.isAnnotationPresent(NonameOption.class)) {
                    throw new Exception("Unable to present mandatory argument (" + getFieldDescription(field)+")");
                }
                else {
                    throw new Exception("Unable to present value for mandatory option "+getFieldName(field));
                }
            }
        }        
    }

    protected boolean isFieldOptional(Field field) {
        if(field.isAnnotationPresent(Option.class)) {
            return field.getAnnotation(Option.class).isOptional();
        }        
        else
        if(field.isAnnotationPresent(NonameOption.class)) {
            return field.getAnnotation(NonameOption.class).isOptional();
        }    
        else
        if(field.isAnnotationPresent(ListOption.class)) {
            return field.getAnnotation(ListOption.class).isOptional();
        }    
        else
            return true;
    }

    protected String getFieldDescription(Field field) {
        if(field.isAnnotationPresent(Option.class)) {
            return field.getAnnotation(Option.class).description();
        }        
        else
        if(field.isAnnotationPresent(NonameOption.class)) {
            return field.getAnnotation(NonameOption.class).description();
        }
        else
        if(field.isAnnotationPresent(ListOption.class)) {
            return field.getAnnotation(ListOption.class).description();
        }
        else {
            return null;
        }
    }

    protected String getFieldName(Field field) {
        if(field.isAnnotationPresent(Option.class)) {
            return field.getAnnotation(Option.class).name();
        }        
        if(field.isAnnotationPresent(ListOption.class)) {
            return field.getAnnotation(ListOption.class).name();
        }
        else
            return null;
    }

    protected String getFieldShortName(Field field) {
        if(field.isAnnotationPresent(Option.class)) {
            return field.getAnnotation(Option.class).shortName();
        }        
        else
        if(field.isAnnotationPresent(ListOption.class)) {
            return field.getAnnotation(ListOption.class).shortName();
        }
        else
            return null;
    }

    protected Object getFieldValue(Object object, Field field) throws Exception {
        String methodName = "get"+field.getName().toUpperCase().substring(0,1)+field.getName().substring(1);
        Method method = findMethodForField(methodName,object,null);
        return method.invoke(object,(java.lang.Object[])null);
    }
    
    protected void setFieldValue(Object object, Field field, Object param) throws Exception {
        String methodName = "set"+field.getName().toUpperCase().substring(0,1)+field.getName().substring(1);
        Method method = findMethodForField(methodName,object,param);
        method.invoke(object,param);
    }
 
    protected Method findMethodForField(String methodName, Object object, Object param) throws NoSuchMethodException {
        try {
            return object.getClass().getMethod(methodName, new Class[] {param!=null ? param.getClass():null});
        }
        catch(NoSuchMethodException ex) {
            Method[] methods = object.getClass().getMethods();
            for(Method method : methods) {
                if(method.getName().equalsIgnoreCase(methodName)) {
                    return method;
                }
            }
            throw ex;
        }
    }    
    
    /**
     * Print to stream help for specified options
     * <p>Example:</p>
     * @code
     *   parser.printHelp(MyClass.class,System.out);
     * @endcode
     * @param objClass An object which contains of annotated properties. 
     * @param stream Stream to write help
     */
    public <T> void printHelp(Class<T> objClass, PrintStream stream) throws Exception {
        for(Field field  : objClass.getDeclaredFields()) {
            String fieldHelp = getHelpForField(field);
            if(fieldHelp!=null) {
                stream.println(fieldHelp);
            }
        }
    }
    
    protected String getHelpForField(Field field) {
        String name = getFieldName(field), shortName = getFieldShortName(field);
        Class objClass = field.getType();        
        if(name == null) {
            if(!field.isAnnotationPresent(NonameOption.class))
                return null;
            else
                name = "";
        }
        boolean isOptional = isFieldOptional(field);
        boolean hasArgument = !objClass.equals(Boolean.class);
        
        if(field.isAnnotationPresent(NonameOption.class)) {
            return String.format(
                "%1$37s","" + (isOptional ? "[" : "") + "<value>" + (isOptional ? "[" : "") + " : ")
                + getFieldDescription(field);
        }
        else
            return String.format(
                "%1$18s %2$8s %3$6s : %4$s", 
                "" + (isOptional ? "[" : "") + name+",", 
                shortName + (isOptional ? "]":""), 
                (hasArgument ? " <arg>": ""),
                getFieldDescription(field)
            );
    }
}
